package sm.codec;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Stack;

import org.apache.mina.core.buffer.IoBuffer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import sm.codec.api.Codec;
import sm.codec.exception.DecodeException;
import sm.codec.exception.EncodeException;
import sm.codec.type.Logical;
import sm.codec.type.NullValue;
import sm.codec.type.RC;
import sm.codec.type.Struct;
import sm.dbdict.DBDictUtil;
import sm.dbdict.def.DBDict;
import sm.dbdict.def.Field;
import sm.dbdict.def.FieldCheckCode;
import sm.dbdict.def.FileOptions;
import sm.dbdict.def.Key;
import sm.dbdict.def.SqlFieldOptions;
import sm.dbdict.def.SqlIndex;
import sm.dbdict.def.SqlLookupTableKeys;
import sm.dbdict.def.SqlTables;
import sm.dbdict.def.StructureArrayOptions;
import sm.def.Type;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class DBDictCodec extends RCCodec {

	private static Logger logger = LoggerFactory.getLogger(Codec.class);

	private Object rc2POJO(Object obj) {
		if (obj instanceof NullValue) {
			return null;
		} else if (obj instanceof Logical) {
			if (obj == Logical.TRUE) {
				return Boolean.TRUE;
			} else if (obj == Logical.FALSE) {
				return Boolean.FALSE;
			} else {
				return null;
			}
		} else if (obj instanceof Struct) {
			List<Object> newList = new ArrayList<Object>();
			List<Object> objects = ((Struct) obj).getValue();
			for (Object object : objects) {
				newList.add(rc2POJO(object));
			}
			return newList;
		} else {
			return obj;
		}
	}

	private Object pojo2RC(Object obj, Class<?> type) {
		if (obj == null) {
			if (type.isAssignableFrom(Double.class)) {
				return NullValue.NumberNull;
			} else if (type.isAssignableFrom(String.class)) {
				return NullValue.CharacterNull;
			} else if (type.isAssignableFrom(Date.class)) {
				return NullValue.TimeNull;
			} else if (type.isAssignableFrom(Boolean.class)) {
				return NullValue.LogicalNull;
			} else {
				return null;
			}
		} else {
			if (obj instanceof Boolean) {
				Boolean bool = (Boolean) obj;
				if (bool) {
					return Logical.TRUE;
				} else {
					return Logical.FALSE;
				}
			} else if (obj instanceof Number) {
				return ((Number) obj).doubleValue();
			}

			else {
				return obj;
			}

		}
	}

	private List<FieldCheckCode> constructFieldCheckCode(List<Field> fields) {
		List<FieldCheckCode> checkCodes = new ArrayList<FieldCheckCode>();

		Stack<Object[]> stack = new Stack<Object[]>();
		for (Field field : fields) {
			// set type
			int type = field.getType();
			FieldCheckCode checkCode = new FieldCheckCode();
			if (type == Type.ARRAY_TYPE) {
				checkCode.setType(1);
			} else if (type != Type.STRUCTURE_TYPE) {
				checkCode.setType(0);
			}
			checkCodes.add(checkCode);

			// handle parent
			int level = field.getLevel();
			boolean checkRelatives = !stack.empty();
			while (checkRelatives) {
				Object[] relative = stack.pop();
				Field relativeField = (Field) relative[0];
				int relativeLevel = relativeField.getLevel();
				FieldCheckCode relativeCheckCode = (FieldCheckCode) relative[1];

				if (level > relativeLevel) { // child, do nothing and break
					checkRelatives = false;
					stack.push(relative);
				} else {
					checkRelatives = true;

					Object[] parent = stack.peek();
					Field parentField = (Field) parent[0];
					FieldCheckCode parentCheckCode = (FieldCheckCode) parent[1];

					// add size to relative
					int relativeSize = relativeCheckCode.getSize() + 1;
					relativeCheckCode.setSize(relativeSize);

					if (level < relativeLevel) { // uncle, set brother's type
						relativeCheckCode.setSize(0);
						if (parentField.getType() == Type.STRUCTURE_TYPE) {
							parentCheckCode.setType(relativeField.getIndex());
						}
					}

					// add size to parent
					int parentSize = parentCheckCode.getSize();
					parentCheckCode.setSize(parentSize + relativeSize);
				}
			}

			// push to stack
			stack.push(new Object[] { field, checkCode });
		}

		boolean checkRelatives = !stack.empty();
		while (checkRelatives) {
			Object[] parent = stack.pop();
			Field parentField = (Field) parent[0];
			FieldCheckCode parentCheckCode = (FieldCheckCode) parent[1];
			parentCheckCode.setSize(0);
			checkRelatives = !stack.empty();
			if (checkRelatives) {
				int index = parentField.getIndex();
				parent = stack.peek();
				parentField = (Field) parent[0];
				parentCheckCode = (FieldCheckCode) parent[1];
				if (parentField.getType() == Type.STRUCTURE_TYPE) {
					parentCheckCode.setType(index);
				}
			}
		}

		return checkCodes;
	}

	@Override
	public Object decode(IoBuffer buffer) throws DecodeException {
		RC rc = (RC) super.decode(buffer);
		List<Object> objs = rc.getValues();

		Struct struct = (Struct) objs.get(0); // get descriptor
		List<Object> objects = (List<Object>) rc2POJO(struct);

		DBDict descriptor = new DBDict();
		List<Field> fields = new ArrayList<Field>();
		List<Object> fieldsObj = (List<Object>) objects.get(0);
		if (fieldsObj != null) {
			for (Object fObj : fieldsObj) {
				List<Object> fieldObj = (List<Object>) fObj;
				Field field = new Field();
				String name = (String) fieldObj.get(0);
				field.setName(name);

				Double level = (Double) fieldObj.get(1);
				field.setLevel(level.intValue());

				Double index = (Double) fieldObj.get(2);
				field.setIndex(index.intValue());

				Double type = (Double) fieldObj.get(3);

				// skip old type which is bigger than 16
				// e.g. application.table, type is 22
				if (type > 16) {
					type = type % 16;
				}
				field.setType(type.intValue());

				String structure = (String) fieldObj.get(4);
				field.setStructure(structure);

				String db2FieldName = (String) fieldObj.get(5);
				field.setDb2_field_name(db2FieldName);

				Double db2Length = (Double) fieldObj.get(6);
				if (db2Length != null)
					field.setDb2_length(db2Length.intValue());

				List<Object> sqlFieldOptionsObj = (List<Object>) fieldObj
						.get(7);
				if (sqlFieldOptionsObj != null) {
					SqlFieldOptions sqlFieldOptions = new SqlFieldOptions();
					String sqlTableAlias = (String) sqlFieldOptionsObj.get(0);
					sqlFieldOptions.setSql_table_alias(sqlTableAlias);

					String sqlColumnName = (String) sqlFieldOptionsObj.get(1);
					sqlFieldOptions.setSql_column_name(sqlColumnName);

					String sqlDataType = (String) sqlFieldOptionsObj.get(2);
					sqlFieldOptions.setSql_data_type(sqlDataType);

					Boolean sqlRcFormat = (Boolean) sqlFieldOptionsObj.get(3);
					sqlFieldOptions.setSql_rc_format(sqlRcFormat);
					field.setSql_field_options(sqlFieldOptions);
				}

				List<Object> structureArrayOptionsObj = (List<Object>) fieldObj
						.get(8);
				if (structureArrayOptionsObj != null
						&& structureArrayOptionsObj.size() > 0) {
					StructureArrayOptions structureArrayOptions = new StructureArrayOptions();
					Boolean saUniqueKey = (Boolean) structureArrayOptionsObj
							.get(0);
					structureArrayOptions.setSa_unique_key(saUniqueKey);

					String saUniqueFilename = (String) structureArrayOptionsObj
							.get(1);
					structureArrayOptions
							.setSa_unique_filename(saUniqueFilename);

					String saAttributeFile = (String) structureArrayOptionsObj
							.get(2);
					structureArrayOptions.setSa_attribute_file(saAttributeFile);

					Boolean temporal = (Boolean) structureArrayOptionsObj
							.get(3);
					structureArrayOptions.setTemporal(temporal);
					field.setStructure_array_options(structureArrayOptions);
				}

				List<Object> fieldCheckCodeObj = (List<Object>) fieldObj.get(9);
				if (fieldCheckCodeObj != null) {
					FieldCheckCode fieldCheckCode = new FieldCheckCode();
					Double fieldCheckCodeType = (Double) fieldCheckCodeObj
							.get(0);
					if (fieldCheckCodeType != null)
						fieldCheckCode.setType(fieldCheckCodeType.intValue());
					Double fieldCheckCodeSize = (Double) fieldCheckCodeObj
							.get(1);
					if (fieldCheckCodeSize != null)
						fieldCheckCode.setSize(fieldCheckCodeSize.intValue());
					field.setFieldCheckCode(fieldCheckCode);
				}
				// ignore field check code
				fields.add(field);
			}
			descriptor.setField(fields);
		}

		List<Object> keysObj = (List<Object>) objects.get(1);
		if (keysObj != null) {
			List<Key> keys = new ArrayList<Key>();
			for (Object kObj : keysObj) {
				List<Object> keyObj = (List<Object>) kObj;
				Key key = new Key();
				Double flags = (Double) keyObj.get(0);
				if (flags != null)
					key.setFlags(flags.intValue());

				List<String> names = (List<String>) keyObj.get(1);
				key.setName(names);

				// key check code
				if (keyObj.size() > 2) {
					List<SqlIndex> checkCodes = new ArrayList<SqlIndex>();
					List<Object> ccs = (List<Object>) keyObj.get(2);
					if (ccs != null) {
						for (Object cc : ccs) {
							List<Object> cCode = (List<Object>) cc;
							SqlIndex checkCode = new SqlIndex();
							checkCode.setTableName((String) cCode.get(0));
							checkCode.setIndexName((String) cCode.get(1));
							Double ccType = (Double) cCode.get(2);
							if (ccType != null)
								checkCode.setType(ccType.intValue());
							checkCodes.add(checkCode);
						}
					}
					key.setSqlIndexes(checkCodes);
				}

				keys.add(key);
			}
			descriptor.setKey(keys);
		}

		Object name = objects.get(2);
		descriptor.setName((String) name);

		Double indexFile = (Double) objects.get(3);
		if (indexFile != null)
			descriptor.setIndex_file(indexFile.intValue());

		Double indexRecLen = (Double) objects.get(4);
		if (indexRecLen != null)
			descriptor.setIndex_rec_len(indexRecLen.intValue());

		List<Integer> indexPools = new ArrayList<Integer>();
		for (Object obj : (List<Object>) objects.get(5)) {
			if (obj != null) {
				int indexPool = ((Double) obj).intValue();
				indexPools.add(indexPool);
			}
		}
		descriptor.setIndex_pools(indexPools);

		Double dataFile = (Double) objects.get(6);
		if (dataFile != null)
			descriptor.setData_file(dataFile.intValue());

		Double dataRecLen = (Double) objects.get(7);
		if (dataRecLen != null)
			descriptor.setData_rec_len(dataRecLen.intValue());

		List<Integer> dataPools = new ArrayList<Integer>();
		for (Object obj : (List<Object>) objects.get(8)) {
			if (obj != null) {
				int dataPool = ((Double) obj).intValue();
				dataPools.add(dataPool);
			}
		}
		descriptor.setData_pools(dataPools);

		Double rootRecord = (Double) objects.get(9);
		if (rootRecord != null)
			descriptor.setRoot_record(rootRecord.intValue());

		List<Object> sqlTablesObj = (List<Object>) objects.get(10);
		if (sqlTablesObj != null) {
			List<SqlTables> sqlTables = new ArrayList<SqlTables>();
			for (Object sObj : sqlTablesObj) {
				List<Object> sqlTableObj = (List<Object>) sObj;
				SqlTables sqlTable = new SqlTables();
				Object sqlDbType = sqlTableObj.get(0);
				sqlTable.setSql_db_type((String) sqlDbType);

				Object sqlTableName = sqlTableObj.get(1);
				sqlTable.setSql_table_name((String) sqlTableName);

				Object sqlTableAlias = sqlTableObj.get(2);
				sqlTable.setSql_table_alias((String) sqlTableAlias);

				List<Object> sqlLookupTableKeysObj = (List<Object>) sqlTableObj
						.get(3);
				if (sqlLookupTableKeysObj != null) {
					List<SqlLookupTableKeys> sqlLookupTableKeys = new ArrayList<SqlLookupTableKeys>();
					for (Object sqlObj : sqlLookupTableKeysObj) {
						List<Object> sqlLookupTableKeyObj = (List<Object>) sqlObj;
						SqlLookupTableKeys sqlLookupTableKey = new SqlLookupTableKeys();
						Object sqlLookupField = sqlLookupTableKeyObj.get(0);
						sqlLookupTableKey
								.setSqlLookupField((String) sqlLookupField);

						Object sqlLookupColumn = sqlLookupTableKeyObj.get(1);
						sqlLookupTableKey
								.setSqlLookupColumn((String) sqlLookupColumn);
						sqlLookupTableKeys.add(sqlLookupTableKey);
					}
					sqlTable.setSql_lookup_table_keys(sqlLookupTableKeys);
				}

				List<String> sqlTableOptions = (List<String>) sqlTableObj
						.get(4);
				sqlTable.setSql_table_options(sqlTableOptions);

				sqlTables.add(sqlTable);
			}
			descriptor.setSql_tables(sqlTables);
		}

		List<Object> fileOptionsObj = (List<Object>) objects.get(11);
		FileOptions fileOptions = new FileOptions();
		Object shadow = fileOptionsObj.get(0);
		fileOptions.setShadow((Boolean) shadow);

		Object reselect = fileOptionsObj.get(1);
		fileOptions.setReselect((Boolean) reselect);

		descriptor.setFile_options(fileOptions);

		Object caseInsensitive = objects.get(12);
		descriptor.setCase_insensitive((Boolean) caseInsensitive);

		Object sysmodtime = objects.get(13);
		descriptor.setSysmodtime((Date) sysmodtime);

		Object sysmoduser = objects.get(14);
		descriptor.setSysmoduser((String) sysmoduser);

		Double sysmodcount = (Double) objects.get(15);
		if (sysmodcount != null)
			descriptor.setSysmodcount(sysmodcount.intValue());

		if (logger.isDebugEnabled()) {
			Gson gson = new GsonBuilder().setPrettyPrinting().create();
			logger.debug("decode DBDict: " + gson.toJson(descriptor));
		}

		// set parent field
		DBDictUtil.setParentField(descriptor);
		return descriptor;
	}

	@Override
	public void encode(IoBuffer buffer, Object value) throws EncodeException {
		DBDict des = (DBDict) value;
		if (logger.isDebugEnabled()) {
			Gson gson = new GsonBuilder().setPrettyPrinting().create();
			logger.debug("encode DBDict: " + gson.toJson(des));
		}
		List<Object> rc = new ArrayList<Object>();

		List<Object> descriptorFields = new ArrayList<Object>();
		Struct descriptorObj = new Struct(descriptorFields, Type.STRUCTURE_TYPE); // descriptor

		List<Object> fields = new ArrayList<Object>();
		Struct fieldsObj = new Struct(fields, Type.ARRAY_TYPE);
		List<Field> fs = des.getField();
		if (fs != null) {
			List<FieldCheckCode> ccs = constructFieldCheckCode(fs);

			for (int i = 0; i < fs.size(); i++) {
				Field f = fs.get(i);
				List<Object> field = new ArrayList<Object>();
				Struct fieldObj = new Struct(field, Type.STRUCTURE_TYPE);
				field.add(pojo2RC(f.getName(), String.class));
				field.add(pojo2RC(f.getLevel(), Double.class));
				field.add(pojo2RC(f.getIndex(), Double.class));
				field.add(pojo2RC(f.getType(), Double.class));
				field.add(pojo2RC(f.getStructure(), String.class));
				field.add(pojo2RC(f.getDb2_field_name(), String.class));
				field.add(pojo2RC(f.getDb2_length(), String.class));

				List<Object> sqlFieldOptions = new ArrayList<Object>();
				Struct sqlFieldOptionsObj = new Struct(sqlFieldOptions,
						Type.STRUCTURE_TYPE);
				SqlFieldOptions sfo = f.getSql_field_options();
				sqlFieldOptions.add(pojo2RC(sfo.getSql_table_alias(),
						String.class));
				sqlFieldOptions.add(pojo2RC(sfo.getSql_column_name(),
						String.class));
				sqlFieldOptions.add(pojo2RC(sfo.getSql_data_type(),
						String.class));
				sqlFieldOptions.add(pojo2RC(sfo.getSql_rc_format(),
						Boolean.class));
				field.add(sqlFieldOptionsObj);

				List<Object> structureArrayOptions = new ArrayList<Object>();
				Struct structureArrayOptionsObj = new Struct(
						structureArrayOptions, Type.STRUCTURE_TYPE);
				StructureArrayOptions sao = f.getStructure_array_options();
				structureArrayOptions.add(pojo2RC(sao.getSa_unique_key(),
						Boolean.class));
				structureArrayOptions.add(pojo2RC(sao.getSa_unique_filename(),
						String.class));
				structureArrayOptions.add(pojo2RC(sao.getSa_attribute_file(),
						String.class));
				structureArrayOptions.add(pojo2RC(sao.getTemporal(),
						Boolean.class));
				field.add(structureArrayOptionsObj);

				// field check code
				List<Object> checkCodes = new ArrayList<Object>();
				Struct checkCodesObj = new Struct(checkCodes,
						Type.STRUCTURE_TYPE);
				FieldCheckCode cc = ccs.get(i);
				checkCodes.add(pojo2RC(cc.getType(), Double.class));
				checkCodes.add(pojo2RC(cc.getSize(), Double.class));
				field.add(checkCodesObj);

				fields.add(fieldObj);
			}
		}
		descriptorFields.add(fieldsObj);

		List<Object> keys = new ArrayList<Object>();
		Struct keysObj = new Struct(keys, Type.ARRAY_TYPE);
		List<Key> ks = des.getKey();
		if (ks != null) {
			for (Key k : ks) {
				List<Object> key = new ArrayList<Object>();
				Struct keyObj = new Struct(key, Type.STRUCTURE_TYPE);
				key.add(pojo2RC(k.getFlags(), Double.class));

				List<Object> names = new ArrayList<Object>();
				Struct namesObj = new Struct(names, Type.ARRAY_TYPE);
				List<String> ns = k.getName();
				if (ns != null) {
					for (String n : ns) {
						names.add(pojo2RC(n, String.class));
					}
				}
				key.add(namesObj);

				List<Object> checkCodes = new ArrayList<Object>();
				Struct checkCodesObj = new Struct(checkCodes, Type.ARRAY_TYPE);
				List<SqlIndex> kccs = k.getSqlIndexes();
				if (kccs != null) {
					for (SqlIndex kcc : kccs) {
						List<Object> checkCode = new ArrayList<Object>();
						Struct checkCodeObj = new Struct(checkCode,
								Type.STRUCTURE_TYPE);
						checkCode
								.add(pojo2RC(kcc.getTableName(), String.class));
						checkCode
								.add(pojo2RC(kcc.getIndexName(), String.class));
						checkCode.add(pojo2RC(kcc.getType(), Double.class));
						checkCodes.add(checkCodeObj);
					}
				}
				key.add(checkCodesObj);

				keys.add(keyObj);
			}
		}
		descriptorFields.add(keysObj);

		descriptorFields.add(pojo2RC(des.getName(), String.class));

		descriptorFields.add(pojo2RC(des.getIndex_file(), String.class));

		descriptorFields.add(pojo2RC(des.getIndex_rec_len(), String.class));

		List<Object> indexPools = new ArrayList<Object>();
		Struct indexPoolsObj = new Struct(indexPools, Type.ARRAY_TYPE);
		List<Integer> ips = des.getIndex_pools();
		if (ips != null) {
			for (Integer ip : ips) {
				indexPools.add(pojo2RC(ip, Double.class));
			}
		}
		descriptorFields.add(indexPoolsObj);

		descriptorFields.add(pojo2RC(des.getData_file(), String.class));

		descriptorFields.add(pojo2RC(des.getData_rec_len(), String.class));

		List<Object> dataPools = new ArrayList<Object>();
		Struct dataPoolsObj = new Struct(dataPools, Type.ARRAY_TYPE);
		List<Integer> dps = des.getData_pools();
		if (dps != null) {
			for (Integer dp : dps) {
				dataPools.add(pojo2RC(dp, Double.class));
			}
		}
		descriptorFields.add(dataPoolsObj);

		descriptorFields.add(pojo2RC(des.getRoot_record(), Double.class));

		List<Object> sqlTables = new ArrayList<Object>();
		Struct sqlTablesObj = new Struct(sqlTables, Type.ARRAY_TYPE);
		List<SqlTables> sts = des.getSql_tables();
		if (sts != null) {
			for (SqlTables st : sts) {
				List<Object> sqlTable = new ArrayList<Object>();
				Struct sqlTableObj = new Struct(sqlTable, Type.STRUCTURE_TYPE);
				sqlTable.add(pojo2RC(st.getSql_db_type(), String.class));
				sqlTable.add(pojo2RC(st.getSql_table_name(), String.class));
				sqlTable.add(pojo2RC(st.getSql_table_alias(), String.class));

				List<SqlLookupTableKeys> slks = st.getSql_lookup_table_keys();
				if (slks != null && slks.size() > 0) {
					List<Object> sqlLookupTableKeys = new ArrayList<Object>();
					Struct sqlLookupTableKeysObj = new Struct(
							sqlLookupTableKeys, Type.ARRAY_TYPE);
					for (SqlLookupTableKeys slk : slks) {
						List<Object> sqlLookupTableKey = new ArrayList<Object>();
						Struct sqlLookupTableKeyObj = new Struct(
								sqlLookupTableKey, Type.STRUCTURE_TYPE);
						sqlLookupTableKey.add(pojo2RC(slk.getSqlLookupField(),
								String.class));
						sqlLookupTableKey.add(pojo2RC(slk.getSqlLookupColumn(),
								String.class));
						sqlLookupTableKeys.add(sqlLookupTableKeyObj);
					}
					sqlTable.add(sqlLookupTableKeysObj);
				} else {
					sqlTable.add(NullValue.CharacterNull);
				}

				List<String> stos = st.getSql_table_options();
				if (stos != null && stos.size() > 0) {
					List<Object> sqlTableOptions = new ArrayList<Object>();
					Struct sqlTableOptionsObj = new Struct(sqlTableOptions,
							Type.ARRAY_TYPE);
					for (String sto : stos) {
						sqlTableOptions.add(pojo2RC(sto, String.class));
					}
					sqlTable.add(sqlTableOptionsObj);
				} else {
					sqlTable.add(NullValue.CharacterNull);
				}

				sqlTables.add(sqlTableObj);
			}
		}
		descriptorFields.add(sqlTablesObj);

		List<Object> fileOptions = new ArrayList<Object>();
		Struct fileOptionsObj = new Struct(fileOptions, Type.STRUCTURE_TYPE);
		FileOptions fos = des.getFile_options();
		fileOptions.add(pojo2RC(fos.getShadow(), Boolean.class));
		fileOptions.add(pojo2RC(fos.getReselect(), Boolean.class));
		descriptorFields.add(fileOptionsObj);

		descriptorFields.add(pojo2RC(des.getCase_insensitive(), Boolean.class));

		descriptorFields.add(pojo2RC(des.getSysmodtime(), Date.class));

		descriptorFields.add(pojo2RC(des.getSysmoduser(), String.class));

		descriptorFields.add(pojo2RC(des.getSysmodcount(), Double.class));

		rc.add(descriptorObj);

		super.encode(buffer, new RC(rc));
	}
}
